package org.viettan.android.main.xml;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.inject.internal.Nullable;

/**
 * Base class for all XML parsers to provide common utility methods.
 */
public abstract class BaseXmlParser<T> {

    private final String entityTag;

    protected BaseXmlParser(String entityTag) {
	this.entityTag = entityTag;
    }

    /**
     * Parses a feed of XML holding multiple entities of type {@code T} into the a {@link List} of
     * {@code T}.
     *
     * @param xml
     *            full XML source to parse
     * @return parsed entities
     * @throws XmlPullParserException
     * @throws IOException
     */
    public List<T> parseMultiple(String xml) throws XmlPullParserException, IOException {
	return parseMultiple(xml, null);
    }

    /**
     * Parses a feed of XML holding multiple entities of type {@code T} into the a {@link List} of
     * {@code T}.
     *
     * @param xml
     *            full XML source to parse
     * @param parentNodeTagName
     *            if provided will only parse the entities that have this tag name as an ancestor
     * @return parsed entities
     * @throws XmlPullParserException
     * @throws IOException
     */
    public List<T> parseMultiple(String xml, @Nullable String parentNodeTagName)
	    throws XmlPullParserException, IOException {
	if (Strings.isNullOrEmpty(xml)) {
	    return Collections.emptyList();
	}

	XmlPullParser parser = createParser(xml);
	return parseMultiple(parser, parentNodeTagName);
    }

    private List<T> parseMultiple(XmlPullParser parser, @Nullable String parentNodeTagName)
	    throws XmlPullParserException, IOException {
	List<T> entities = Lists.newArrayList();

	// if no parentNodeTagName is provided, we start capturing all discovered entities
	// right away
	boolean startCapturing = Strings.isNullOrEmpty(parentNodeTagName);

	T currentEntity = null;
	int eventType = parser.getEventType();
	while (eventType != XmlPullParser.END_DOCUMENT) {
	    eventType = parser.next();
	    switch (eventType) {
	    case XmlPullParser.START_TAG:
		String tagName = parser.getName();

		// only capture matched entities when we are told too
		if (startCapturing && tagName.equalsIgnoreCase(entityTag)) {
		    currentEntity = createEntity(parser);
		    entities.add(currentEntity);
		} else if (tagName.equalsIgnoreCase(parentNodeTagName)) {
		    // we discovered the parent node tag after which we are supposed
		    // to start capturing
		    startCapturing = true;
		}

		if (currentEntity == null) {
		    continue;
		}

		parseProperty(currentEntity, parser);
		break;
	    }
	}

	return entities;
    }

    /**
     * Parses XML that holds only a single entity.
     *
     * @param xml
     * @return
     * @throws XmlPullParserException
     * @throws IOException
     */
    public T parseSingle(String xml) throws XmlPullParserException, IOException {
	Preconditions.checkArgument(!Strings.isNullOrEmpty(xml), "Xml cannot be null or empty");

	XmlPullParser parser = createParser(xml);
	return parseSingle(parser);
    }

    // TODO: I think parseSingle and parseMultiple can be merged into one
    // assuming that the XML made up is the same
    private T parseSingle(XmlPullParser parser) throws XmlPullParserException, IOException {
	T entity = createEntity(null);
	int eventType = parser.getEventType();
	while (eventType != XmlPullParser.END_DOCUMENT) {
	    eventType = parser.next();
	    switch (eventType) {
	    case XmlPullParser.START_TAG:
		parseProperty(entity, parser);
		break;
	    }
	}

	return entity;
    }

    /**
     * Creates a new {@code XmlPullParser} whose input is already set on the passed XML string.
     *
     * @param xml
     * @return
     * @throws XmlPullParserException
     */
    protected XmlPullParser createParser(String xml) throws XmlPullParserException {
	XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
	parser.setInput(new ByteArrayInputStream(xml.getBytes()), null);
	return parser;
    }

    /**
     * Parses a single property of the entity and populates it into the entity.
     *
     * @param entity
     * @param parser
     * @throws XmlPullParserException
     * @throws IOException
     */
    protected abstract void parseProperty(T entity, XmlPullParser parser)
	    throws XmlPullParserException, IOException;

    /**
     * Creates a new instance of the entity.
     *
     * @return
     */
    protected abstract T createEntity(XmlPullParser parser);
}
